
package de.uni.wuppertal.server.ftp;

import java.io.*;
import java.net.Socket;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import de.uni.wuppertal.server.ErrorLogEntry;
import de.uni.wuppertal.server.InputLogEntry;
import de.uni.wuppertal.server.OutputLogEntry;
import de.uni.wuppertal.server.ui.MainWindow;

/**
 * @author Martin Schröder
 * @author Patrik Nordsiek
 */
public class FTPServerControlConnection extends Thread
{
	private FTPServer server;
	private Socket socket;
	private InputStream inputStream;
	private OutputStream outputStream;
	private BufferedReader reader;
	private BufferedWriter writer;
	
	private MainWindow window;
	private boolean terminate = false;
	
	private FTPServerCommand command;
	private FTPServerConnector connector;
	
	private String username = null;
	private boolean authorized = false;
	private boolean binary = false;
	
	private File rootDirectory;
	private String currentDirectory = "/";
	
	private static DateFormat mdtmDateFormat;
	
	static
	{
		mdtmDateFormat = new SimpleDateFormat("yyyyMMddHHmmss", new Locale("en"));
		mdtmDateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
	}
	
	private static String[] commands =
	{
		"USER", "PASS", "QUIT", "NOOP", "SYST", "HELP", "TYPE", "OPTS", "SITE", "FEAT", "EPSV", "PASV", "PWD", "CWD", "CDUP", "SIZE", "MDTM", "LIST", "RETR"
	};
	
	static
	{
		Arrays.sort(commands);
	}
	
	public FTPServerControlConnection(FTPServer server, Socket socket) throws Throwable
	{
		super();
		
		this.server = server;
		this.socket = socket;
		
		window = MainWindow.getInstance();
		
		socket.setSoTimeout(0);
		
		inputStream = socket.getInputStream();
		outputStream = socket.getOutputStream();
		
		reader = new BufferedReader(new InputStreamReader(inputStream));
		writer = new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8"));
		
		command = new FTPServerCommand();
		
		rootDirectory = new File(server.getRootDirectory());
	}
	
	public String getUsername()
	{
		return username;
	}
	
	public boolean isAuthorized()
	{
		return authorized;
	}
	
	public boolean isBinary()
	{
		return binary;
	}
	
	public File getRootDirectory()
	{
		return rootDirectory;
	}
	
	public String getCurrentDirectory()
	{
		return currentDirectory;
	}
	
	public File getWorkingDirectory()
	{
		return new File(rootDirectory.getAbsolutePath() + "/" + currentDirectory);
	}
	
	public synchronized void setTerminate(boolean terminate)
	{
		this.terminate = terminate;
	}
	
	@Override
	public void run()
	{
		try
		{
			terminate = false;
			
			sendStatus(220, "Hello and welcome to Uni Wuppertal FTP");
			
			while(!terminate)
			{
				readCommand();
				
				if(command.is("QUIT"))
				{
					sendStatus(200, "Goodbye");
					
					break;
				}
				else if(command.is("USER"))
				{
					sendStatus(230);
				}
				else if(command.is("PORT"))
				{
					sendStatus(510, "The PORT command is disabled use PASV instead");
				}
				else if(command.is("NOOP"))
				{
					sendStatus(200);
				}
				else if(command.is("SYST"))
				{
					sendStatus(215, "UNIX Type: L8");
				}
				else if(command.is("HELP"))
				{
					execHelpCommand();
				}
				else if(command.is("TYPE"))
				{
					execTypeCommand();
				}
				else if(command.is("OPTS"))
				{
					execOptionsCommand();
				}
				else if(command.is("SITE"))
				{
					execSiteCommand();
				}
				else if(command.is("FEAT"))
				{
					execFeatCommand();
				}
				else if(command.is("EPSV"))
				{
					try
					{
						establishDataConnection(true);
					}
					catch(Throwable e)
					{
						sendStatus(500, e);
					}
				}
				else if(command.is("PASV"))
				{
					try
					{
						establishDataConnection(false);
					}
					catch(Throwable e)
					{
						sendStatus(500, e);
					}
				}
				else if(command.is("PWD"))
				{
					sendStatus(257, "\"" + currentDirectory + "\"");
				}
				else if(command.is("CWD"))
				{
					currentDirectory = command.getArgument(0);
					
					sendStatus(250, "Directory changed to \"" + currentDirectory + "\"");
				}
				else if(command.is("CDUP"))
				{
					StringBuffer buffer = new StringBuffer();
					String[] parts = currentDirectory.split("/");
					
					if(parts.length < 2)
					{
						sendStatus(550);
					}
					else
					{
						for(int i = 0; i < parts.length - 1; i++)
						{
							buffer.append(parts[i]);
							
							if(i < parts.length - 2)
							{
								buffer.append('/');
							}
						}
						
						currentDirectory = buffer.toString();
						
						sendStatus(250, "Directory changed to \"" + currentDirectory + "\"");
					}
				}
				else if(command.is("SIZE"))
				{
					File target = getTargetFile(command.getArgument(0));
					
					if(target.exists() && target.isFile())
					{
						sendStatus(213, String.valueOf(target.length()));
					}
					else
					{
						sendStatus(550, "No such file");
					}
				}
				else if(command.is("MDTM"))
				{
					File target = getTargetFile(command.getArgument(0));
					
					if(target.exists())
					{
						sendStatus(213, mdtmDateFormat.format(new Date(target.lastModified())));
					}
					else
					{
						sendStatus(550, "resource not found \"" + command.getArgument(0) + "\"");
					}
				}
				else if(command.is("LIST"))
				{
					execListCommand();
				}
				else if(command.is("RETR"))
				{
					execRetrieveCommand();
				}
				else
				{
					sendStatus(502, "Command not implemented: " + command.getCommand());
				}
			}
		}
		catch(FTPServerException e)
		{
			window.log(new ErrorLogEntry(e));
			
			try
			{
				sendStatus(e.getStatus());
			}
			catch(Throwable ex)
			{
				window.log(new ErrorLogEntry(ex));
			}
		}
		catch(Throwable e)
		{
			window.log(new ErrorLogEntry(e));
			
			try
			{
				sendStatus(451);
			}
			catch(Throwable ex)
			{
				window.log(new ErrorLogEntry(ex));
			}
		}
		finally
		{
			try
			{
				socket.close();
				
				if(connector != null)
				{
					connector.setTerminate(true);
					
					if(connector.isAlive())
					{
						connector.join();
					}
					
					connector.close();
					connector = null;
				}
			}
			catch(Throwable e)
			{
			}
		}
	}
	
	private void execHelpCommand() throws Throwable
	{
		sendLine("214-The following commands are recognized.");
		
		StringBuffer buffer = new StringBuffer();
		
		for(String cmd : commands)
		{
			buffer.append(' ');
			buffer.append(cmd);
		}
		
		sendLine(buffer.toString());
		
		sendStatus(214, "Help OK");
	}
	
	private void execFeatCommand() throws Throwable
	{
		sendLine("211-Extensions supported");
		sendLine(" SIZE");
		sendLine(" MDTM");
		sendStatus(211, "end");
	}
	
	private void execListCommand() throws Throwable
	{
		FTPServerDataConnection conn;
		
		try
		{
			conn = getDataConnection();
		}
		catch(Throwable e)
		{
			sendStatus(500, "Passive mode required: " + e.getMessage());
			
			return;
		}
		
		try
		{
			sendStatus(150, "Here comes the directory listing.");
			
			conn.execListCommand(getWorkingDirectory());
			
			sendStatus(226, "Directory send OK.");
		}
		catch(Throwable t)
		{
			sendStatus(551, "File listing failed");
		}
	}
	
	private void execRetrieveCommand() throws Throwable
	{
		File target = getTargetFile(command.getArgument(0));
		
		if(!target.exists() || !target.isFile())
		{
			sendStatus(550, "No such file \"" + command.getArgument(0) + "\"");
			
			return;
		}
		
		FTPServerDataConnection conn;
		
		try
		{
			conn = getDataConnection();
		}
		catch(Throwable e)
		{
			sendStatus(425, "Passive mode required: " + e.getMessage());
			
			return;
		}
		
		try
		{
			sendStatus(150, "Start sending file data");
			
			conn.execRetrieveCommand(target);
			
			sendStatus(226, "File transfer OK");
		}
		catch(Throwable t)
		{
			sendStatus(426, t.getMessage());
		}
	}
	
	private File getTargetFile(String location)
	{
		if(location.indexOf("./") >= 0 || location.indexOf("\\") >= 0)
		{
			throw new FTPServerException("Access to " + location + " denied");
		}
		
		if(location.startsWith("/"))
		{
			return new File(rootDirectory.getAbsolutePath() + location);
		}
		
		return new File(getWorkingDirectory().getAbsolutePath() + "/" + location);
	}
	
	private void execSiteCommand() throws Throwable
	{
		String cmd;
		
		try
		{
			cmd = command.getArgument(0).toLowerCase();
		}
		catch(Throwable t)
		{
			sendStatus(501, "Unrecognized SITE command.");
			
			return;
		}
		
		if(cmd.equals("help"))
		{
			sendStatus(214, "HELP");
		}
		else
		{
			sendStatus(501, "Unrecognized SITE command.");
		}
	}
	
	private void execOptionsCommand() throws Throwable
	{
		sendStatus(200);
	}
	
	private void execTypeCommand() throws Throwable
	{
		String flag;
		
		try
		{
			flag = command.getArgument(0);
		}
		catch(Throwable t)
		{
			sendStatus(504, "No type given");
			
			return;
		}
		
		if(flag.equals("I"))
		{
			binary = true;
		}
		else if(flag.equals("A"))
		{
			binary = false;
		}
		else
		{
			sendStatus(504, "Unrecognized type");
			
			return;
		}
		
		sendStatus(200);
	}
	
	private void establishDataConnection(boolean epsv) throws Throwable
	{
		if(connector != null)
		{
			connector.setTerminate(true);
			
			if(connector.isAlive())
			{
				connector.join();
			}
			
			connector.close();
			connector = null;
		}
		
		final int port = 40000 + (int)(Math.random() * 50);
		
		connector = new FTPServerConnector(server, port);
		connector.start();
		
		if(epsv)
		{
			sendStatus(229, "Entering Extended Passive Mode (|||" + port + "|)");
		}
		else
		{
			String[] address = server.getAddress().getHostAddress().split("\\.", 4);
			
			StringBuffer buffer = new StringBuffer(128);
			
			buffer.append("Entering Passive Mode (");
			buffer.append(address[0]);
			buffer.append(',');
			buffer.append(address[1]);
			buffer.append(',');
			buffer.append(address[2]);
			buffer.append(',');
			buffer.append(address[3]);
			buffer.append(',');
			buffer.append(port / 256);
			buffer.append(',');
			buffer.append(port % 256);
			buffer.append(')');
			
			sendStatus(227, buffer.toString());
		}
	}
	
	private FTPServerDataConnection getDataConnection() throws Throwable
	{
		if(connector == null)
		{
			throw new RuntimeException("No PASV or EPSV request has been made");
		}
		
		connector.join();
		
		if(!connector.isConnected())
		{
			throw new RuntimeException("Client not connected to data connection");
		}
		
		return connector.getConnection();
	}
	
	private String readLine() throws Throwable
	{
		String line;
		
		while(true)
		{
			if(socket.isClosed())
			{
				throw new FTPServerException("Verbindung zum Klienten geschlossen");
			}
			
			try
			{
				line = reader.readLine();
			}
			catch(Throwable e)
			{
				line = null;
			}
			
			if(line != null)
			{
				line = line.trim();
				
				if(line.length() > 0)
				{
					break;
				}
			}
			
			try
			{
				Thread.sleep(100);
			}
			catch(InterruptedException e)
			{
			}
		}
		
		window.log(new InputLogEntry("FTP - " + line));
		
		return line;
	}
	
	private void readCommand() throws Throwable
	{
		command.parseLine(readLine());
	}
	
	public void sendLine(String line) throws Throwable
	{
		if(socket.isClosed())
		{
			throw new FTPServerException("Verbindung zum Klienten geschlossen");
		}
		
		writer.write(line);
		writer.write(FTPServer.CRLF);
		writer.flush();
		
		window.log(new OutputLogEntry("FTP - " + line));
	}
	
	public void sendStatus(int status) throws Throwable
	{
		sendLine(server.getStatusMessage(status));
	}
	
	public void sendStatus(int status, Object message) throws Throwable
	{
		sendLine(status + " " + message);
	}
	
	public void sendStatus(int status, Throwable cause) throws Throwable
	{
		sendLine(status + " " + cause.getMessage());
	}
	
	public void throwStatus(int status) throws FTPServerException
	{
		throw new FTPServerException(status);
	}
	
	public void throwStatus(int status, String message) throws FTPServerException
	{
		throw new FTPServerException(message, status);
	}
}
